import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;

public class Server extends Thread{
	private ArrayList<String> q = new ArrayList<String>();
	private ArrayList<Integer> serverNeedsToSync = new ArrayList<Integer>();
	private HashMap<Integer, Integer> queue = new HashMap<Integer, Integer>();
	private HashMap<Integer, Integer> clock = new HashMap<Integer, Integer>();
	
	
	private int[] serverIPs = {5000, 5001, 5002, 5003, 5004};
	private int myIP;
	private DataOutputStream output;
	private String command = "";	
	private String syncCommand = "";
	private boolean processing = false;
	private MovieTheater movieTheater;
	private boolean serverStatus = false;
		
	public Server(int myIP) {
		this.myIP = myIP;
		for(int i = 0; i < serverIPs.length; i++) {
			queue.put(serverIPs[i], -1);
			clock.put(serverIPs[i], -1);
		}	
		queue.put(myIP, -1);
		clock.put(myIP, 0);
		
		movieTheater = new MovieTheater(this);
		movieTheater.start();
		
		getMaxClock();
		
		
	}
	
	public void updateClock(int process, int timestamp) {
		int maxClock = clock.get(process) > timestamp ? clock.get(process) : timestamp;
		clock.put(process, maxClock);		
		maxClock = clock.get(myIP) > timestamp ? clock.get(myIP) : timestamp; 
		clock.put(myIP, ++maxClock);		
	}
	
	public void output(String out) {
		try {
			output.writeBytes(out + "\n");
			output.flush();
			System.out.println("Outputing => " + out);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void setQueue(int ip, int value){
		queue.put(ip, value);
	}
	
	public void setClock(int ip, int value){
		clock.put(ip, value);
	}
	
	public int getClock(int ip){
		return clock.get(ip);
	}
	
	public int getQueue(int ip){
		return queue.get(ip);
	}
	
	public int[] getIPs(){
		return this.serverIPs;
	}
	
	public int getIP(){
		return myIP;
	}
	
	public void broadcastMsg(String msg) {		
		System.out.println("broadcasting msg => " + msg);
		for(int i = 0; i < serverIPs.length; i++) {			
			if(serverIPs[i]!=myIP) {
				sendMsg(serverIPs[i], msg);
			}
		}
	}
	
	public void sendMsg(int dest, String msg) {
		DataOutputStream out;		
		Socket server = null;
		System.out.println("sendMsg " + dest);
		// get socket
		try {			
			SocketAddress sockaddr = new InetSocketAddress("localhost", dest);			
			server = new Socket();
			server.connect(sockaddr, 1000);			
			out = new DataOutputStream(server.getOutputStream());			
			out.writeBytes(msg + "\n");			
			server.close();			
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (SocketTimeoutException e) {
			
		} catch (IOException e) {
			System.out.println("sendMsg EXCEPTION " + dest);
			e.printStackTrace();
		}	
		finally {
			try {
				server.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	public void sendTimeout(DataOutputStream output2) {		
		// get socket
		try {
			output2.writeBytes("timeout \n");
			output2.flush();
			System.out.println("Outputing => timeout");			
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (SocketTimeoutException e) {
			
		} catch (IOException e) {
			e.printStackTrace();
		}					
	}
	
	public synchronized void release() {
		String output = "";
		queue.put(myIP, -1);		
		
		// Check if a new server is up, if it is up, we know we are about to release the CS
		// send the movie state to the server
		if(serverNeedsToSync.size() > 0) {
			Iterator<Integer> itr = serverNeedsToSync.iterator();
			
			String[] seats = movieTheater.getSeatAssignments();
			for(int i = 0; i < seats.length; i++){
				output += seats[i] + " ";
			}
			
			while (itr.hasNext()) {
				int serverIP = itr.next();
				System.out.println("--RELEASE syncing " + serverIP);
				sendMsg(serverIP, "seatssync " + myIP + " " + clock.get(myIP) + " " + output);				
		    }
			
			// Clear out array
			serverNeedsToSync = new ArrayList<Integer>();
		}
		if(command.length() > 0) {
			StringTokenizer st = new StringTokenizer(command);			
			String requstedCommand = st.nextToken().trim();		
			if(requstedCommand.equalsIgnoreCase("reserve") || requstedCommand.equalsIgnoreCase("delete")){
				broadcastMsg("sync " + command);
			}
		}
		broadcastMsg("release " + clock.get(myIP) + " " + myIP);
		movieTheater.setCS(false);
		processing = false;
		command = "";
	}
	
	public boolean needsSync(){
		if(syncCommand != null && syncCommand.length() > 0)
			return true;
		return false;
	}
	
	public String getSync() {
		return syncCommand;
	}
	
	public void clearSync() {
		syncCommand = "";
	}
	
	public void setCommand(String command) {
		this.command = command;
	}
	
	public String getCommand() {
		return command;
	}
		
	public ArrayList<String> getQ(){
		return q;
	}
	
	public void checkServers() {
		DataOutputStream out;		
		Socket server;
		for(int i = 0; i < serverIPs.length; i++){
			if(serverIPs[i] != myIP) {
				// get socket
				try {
					SocketAddress sockaddr = new InetSocketAddress("localhost", serverIPs[i]);
					server = new Socket();			
					server.connect(sockaddr, 500);								
					out = new DataOutputStream(server.getOutputStream());			
					out.writeBytes("ping");
					server.close();
					
				} catch (UnknownHostException e) {					
					// Server is down
					System.out.println(serverIPs[i] + " timed out");
					queue.put(serverIPs[i], -1);
					clock.put(serverIPs[i], -1);
				} catch (SocketTimeoutException e) {
					// Server is down
					System.out.println(serverIPs[i] + " timed out");
					queue.put(serverIPs[i], -1);
					clock.put(serverIPs[i], -1);
				} catch (IOException e) {
					// Server is down
					System.out.println(serverIPs[i] + " timed out");
					queue.put(serverIPs[i], -1);
					clock.put(serverIPs[i], -1);
				}	
			}
		}
	}
	
	public void getMaxClock() {
		DataOutputStream out = null;		
		Socket server;
		int maxClock = 0;  
		boolean someoneInCS = false;
		int processInCS = 0;
		ArrayList<Integer> serversUp = new ArrayList<Integer>();
		
		for(int i = 0; i < serverIPs.length; i++){
			if(serverIPs[i] != myIP) {
				// get socket
				try {
					System.out.println("connecting to " + serverIPs[i]);
					SocketAddress sockaddr = new InetSocketAddress("localhost", serverIPs[i]);
					server = new Socket();			
					server.connect(sockaddr, 300);								
					out = new DataOutputStream(server.getOutputStream());
					BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream()));	
					out.writeBytes("serverup " + myIP + "\n");	
					String response = in.readLine();
					server.close();
					StringTokenizer st = new StringTokenizer(response);
					String command = st.nextToken().trim();
					int process = Integer.parseInt(st.nextToken().trim());
					
					if(command.equalsIgnoreCase("serverack")){										
						String inCS = st.nextToken().trim();					
						int pclock = Integer.parseInt(st.nextToken().trim());
						int pqueue = Integer.parseInt(st.nextToken().trim());
						maxClock = pclock > maxClock ? pclock : maxClock;		
						serversUp.add(process);
						
						clock.put(process, pclock);
						queue.put(process, pqueue);
						
					} else if(command.equalsIgnoreCase("inCS")) {
						System.out.println("--getMaxClock: someone is in CS");
						someoneInCS = true;
						processInCS = process;
					}
				} catch (UnknownHostException e) {					
					// Server is down
					System.out.println(serverIPs[i] + " timed out");
					queue.put(serverIPs[i], -1);
					clock.put(serverIPs[i], -1);
				} catch (SocketTimeoutException e) {
					// Server is down
					System.out.println(serverIPs[i] + " timed out");
					queue.put(serverIPs[i], -1);
					clock.put(serverIPs[i], -1);
				} catch (IOException e) {
					// Server is down
					System.out.println(serverIPs[i] + " timed out");
					queue.put(serverIPs[i], -1);
					clock.put(serverIPs[i], -1);
				}	
			}
		}
		String data = "";
		
		if(someoneInCS){		
			ServerSocket listener = null;
			Socket client = null;
			StringTokenizer st = null;
			String commandtwo = "";
			try {
				listener = new ServerSocket(myIP);
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
			do {
				try {					
					System.out.println("--getMaxClock: waiting for process in CS");	
					client = listener.accept();
					BufferedReader input = new BufferedReader(new InputStreamReader(client.getInputStream()));
					out = new DataOutputStream(client.getOutputStream());
					data = input.readLine();	
					System.out.println("--getMaxClock: CS process responded with " + data);
					st = new StringTokenizer(data);		
					commandtwo = st.nextToken().trim();
					System.out.println("command = " + commandtwo);
					if(commandtwo.equalsIgnoreCase("seatssync")) {
						System.out.println("commandtwo FOUND!");
					} else {
						// Send back a timeout because this server is not ready
						out.writeBytes("timeout \n");
					}
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} while(!commandtwo.equalsIgnoreCase("seatssync"));
			

			try {
				out.close();
				client.close();
				listener.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
				
				
			// Process Seats
			String[] seatAssignment = new String[movieTheater.getNumberofSeats()];
			int i = 0;
			int pid = Integer.parseInt(st.nextToken().trim());
			int pclock = Integer.parseInt(st.nextToken().trim());				
			maxClock = pclock > maxClock ? pclock : maxClock;			
			
			clock.put(pid, pclock);
			
			for(int x = 0; x < seatAssignment.length; x++) {
				seatAssignment[x] = "";
			}
			
			while(st.hasMoreTokens()){
				System.out.println("--getMaxClock: Processing seats");
				seatAssignment[i++] = st.nextToken().trim();
			}
			movieTheater.setSeatAssignments(seatAssignment);
						
		} else {
			// No one is in CS and now pick a random server that is up to get seat information		
			Iterator<Integer> itr = serversUp.iterator();
			
			while (itr.hasNext()) {
				int serverIP = itr.next();
				try {						
					SocketAddress sockaddr = new InetSocketAddress("localhost", serverIP);	
					server = new Socket();
					server.connect(sockaddr, 1000);	
					out = new DataOutputStream(server.getOutputStream());
					BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream()));
					System.out.println("sending ping");
					out.writeBytes("serverrequestsync " + myIP + "\n");
					System.out.println("waiting for pong");
					data = in.readLine();
					server.close();
					
					// Process Seats
					StringTokenizer st = new StringTokenizer(data);	
					String[] seatAssignment = new String[movieTheater.getNumberofSeats()];
					int i = 0;	
					
					for(int x = 0; x < seatAssignment.length; x++) {
						seatAssignment[x] = "";
					}
					
					while(st.hasMoreTokens()){						
						seatAssignment[i++] = st.nextToken().trim();						
					}
					movieTheater.setSeatAssignments(seatAssignment);
				} catch (UnknownHostException e) {					
					// Server is down
					System.out.println(serverIP + " timed out");
				} catch (SocketTimeoutException e) {
					// Server is down
					System.out.println(serverIP + " timed out");
				} catch (IOException e) {
					// Server is down
					System.out.println(serverIP + " timed out");
				}	
		    }
		}
		
		serverStatus = true;
		
		// Tick own clock
		clock.put(myIP, maxClock+1);
		
		// Send back ack
		broadcastMsg("serversynced " + myIP + " " + clock.get(myIP));
		
		System.out.println("Maxclock = " + maxClock);
		if(movieTheater == null) {
			System.out.println("movietheater is null");
		} else {		
			if(movieTheater.getSeatAssignments()!=null){
				String[] seatAssignment = movieTheater.getSeatAssignments();
				for(int i = 0; i < seatAssignment.length; i++) {
					System.out.println(seatAssignment[i]);
				}
			}
		}
		
		Set s = queue.keySet();		
		Iterator itr = s.iterator();
		
		while(itr.hasNext()) {		
			int key = (Integer)(itr.next());
			System.out.println(key + " " + queue.get(key));		
		}
	}
	
	public void run() {
		
		try {
			ServerSocket listener = new ServerSocket(myIP);
			
			// Set status of server to false
			serverStatus = false;
						
			while(true){				
				try {					
					//synchronized(this){
						System.out.println("waiting for msg");
						Socket client = listener.accept();		
						BufferedReader input = new BufferedReader(new InputStreamReader(client.getInputStream()));
						StringTokenizer st = null;
						String data = input.readLine();			
						String command;
						System.out.println("received => " + data);	
						if(data!=null) {
							st = new StringTokenizer(data);			
							command = st.nextToken().trim();		
						} else {
							command = "";
						}
						if(command.equalsIgnoreCase("reserve") || command.equalsIgnoreCase("search") || command.equalsIgnoreCase("delete")){
							if(!processing && serverNeedsToSync.size() == 0) {
								output = new DataOutputStream(client.getOutputStream());
								setCommand(data);
								
								// Tick own clock
								clock.put(myIP, clock.get(myIP)+1);
								
								// Put my request in queue
								queue.put(myIP, clock.get(myIP));
		
								// Send request to all other processes
								broadcastMsg("request " + clock.get(myIP) + " " + myIP);
								
								synchronized(movieTheater) {	
									System.out.println("notifying movieTheater");
									// Check which servers are up and running
									checkServers();
									movieTheater.notify();												
								}
								
								processing = true;
							} else {
								DataOutputStream output2 = new DataOutputStream(client.getOutputStream());
								sendTimeout(output2);								
							}
						} else if(command.equalsIgnoreCase("request")) {
							int timestamp = Integer.parseInt(st.nextToken().trim());
							int process = Integer.parseInt(st.nextToken().trim());
							
							
							// Update clock
							updateClock(process, timestamp);	
							
							//System.out.println("request received from " + process + " with timestamp " + timestamp);
							
							// Update queue
							queue.put(process, timestamp);
							
							// Send ack back				
							System.out.println("Sending ack " + myIP + " " + clock.get(myIP));
							sendMsg(process, "ack " + clock.get(myIP) + " " + myIP);						
							
						} else if(command.equalsIgnoreCase("ack")) {
							int timestamp = Integer.parseInt(st.nextToken().trim());
							int process = Integer.parseInt(st.nextToken().trim());
							
							// Update clock
							updateClock(process, timestamp);
						
							if(serverNeedsToSync.size() == 0) {
								synchronized(movieTheater) {	
									System.out.println("notifying movieTheater");
									checkServers();
									movieTheater.notify();												
								}
							}
						} else if(command.equalsIgnoreCase("release")) {		
							int timestamp = Integer.parseInt(st.nextToken().trim());
							int process = Integer.parseInt(st.nextToken().trim());
							
							// Update clock
							updateClock(process, timestamp);
							
							queue.put(process, -1);
							if(serverNeedsToSync.size() == 0) {
								synchronized(movieTheater) {	
									System.out.println("notifying movieTheater");
									checkServers();
									movieTheater.notify();												
								}
							}
						} else if(command.equalsIgnoreCase("sync")) {						
							System.out.println("Sync received => " + data);
							while(st.hasMoreTokens()) {
								syncCommand += st.nextToken().trim() + " ";
							}						
						} else if(command.equalsIgnoreCase("serverup")){
							int process = Integer.parseInt(st.nextToken().trim());							
							System.out.println("Server is up " + process);
							
							DataOutputStream output3 = new DataOutputStream(client.getOutputStream());							
							
							// Send back ack if not in CS	
							if(!movieTheater.getCS()) {
								output3.writeBytes("serverack " + myIP + " " + movieTheater.getCS() + " " + clock.get(myIP) + " " + queue.get(myIP) + "\n");							
							} else {
								output3.writeBytes("inCS " + myIP + "\n");
							}
							
							// Set server needs to sync
							serverNeedsToSync.add(process);
						} else if(command.equalsIgnoreCase("serverack")){
							int process = Integer.parseInt(st.nextToken().trim());							
							boolean inCS = Boolean.parseBoolean((st.nextToken().trim()));
							int timestamp = Integer.parseInt(st.nextToken().trim());							
							
							clock.put(process, timestamp);
							
							System.out.println("Received serverack from " + process + " " + inCS + " " + timestamp);						
						} else if(command.equalsIgnoreCase("seatssync")){
							System.out.println("seatsync received");
							String[] seatAssignment = new String[movieTheater.getNumberofSeats()];
							int i = 0;
							
							for(int x = 0; x < seatAssignment.length; x++) {
								seatAssignment[x] = "";
							}
							
							while(st.hasMoreTokens()){
								seatAssignment[i++] = st.nextToken().trim();
							}
							movieTheater.setSeatAssignments(seatAssignment);
							serverStatus = true;
							
							// Tick own clock
							clock.put(myIP, clock.get(myIP)+1);
							
							// Send back ack
							broadcastMsg("serversynced " + myIP + " " + clock.get(myIP));
							
						} else if(command.equalsIgnoreCase("serversynced")){
							Integer process = Integer.parseInt(st.nextToken().trim());
							Integer processClock = Integer.parseInt(st.nextToken().trim());
							
							// Initialize the clock and queue
							clock.put(process, processClock);
							queue.put(process, -1);
							
							// Remove from serverNeedsToSync							
							serverNeedsToSync.remove(process);
							
							// Notify movie theater to continue processing if no more servers need to be synced
							if(serverNeedsToSync.size() == 0) {
								synchronized(movieTheater) {	
									System.out.println("notifying movieTheater");
									checkServers();
									movieTheater.notify();												
								}
							}
							
						} else if(command.equalsIgnoreCase("serverrequestsync")){
							System.out.println("serverrequestsync received");
							DataOutputStream output3 = new DataOutputStream(client.getOutputStream());
							String output = "";
							String[] seats = movieTheater.getSeatAssignments();
							for(int i = 0; i < seats.length; i++){
								output += seats[i] + " ";
							}
							output3.writeBytes(output + "\n");
						}
					//}
				} catch (IOException e) {
					e.printStackTrace();
				}				
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
